using System;
using System.Collections.Generic;
using System.Globalization;
using BizElements.Core;

namespace BizElements.BusinessLayer
{
    /// <summary>Utility/facade class that simplifies common offline concurrency management operations.</summary>
    /// <seealso cref="ILockManager"/><seealso cref="LockManagerFactory"/><seealso cref="LockMode"/>
    public static class LockUtil
    {
        #region Pessimistic offline concurrency control: LockManagerFactory + ILockManager facade.

        /// <summary>
        /// Tries to acquire a shared lock for the object specified in the request.
        /// </summary>
        /// <param name="owner">Actor trying to lock the object.</param>
        /// <param name="objectType">Type of the object to lock. May be null if the current instance is only used to lock a single object type.</param>
        /// <param name="objectId">ID of the object to lock.</param>
        /// <returns><b>false</b> if another <see cref="IActor"/> already locked the object; <b>true</b> otherwise.</returns>
        public static bool AcquireLock(IActor owner, string objectType, params object[] objectId)
        {
            return LockManagerFactory.GetManager(objectType).AcquireLock(owner, objectType, objectId);
        }

        /// <summary>
        /// Tries to acquire a lock for the object specified in the request.
        /// </summary>
        /// <param name="owner">Actor trying to lock the object.</param>
        /// <param name="mode">Indicates how an object should be locked. Some managers may only support the <see cref="LockMode.Default"/> mode.</param>
        /// <param name="timeout">When will the lock expire unless prologned/reacquired. If <b>null</b> the default timeout is used.</param>
        /// <param name="objectType">Type of the object to lock. May be null if the current instance is only used to lock a single object type.</param>
        /// <param name="objectId">ID of the object to lock.</param>        
        /// <returns><b>false</b> if another <see cref="IActor"/> already locked the object; <b>true</b> otherwise.</returns>
        public static bool AcquireLock(IActor owner, LockMode mode, TimeSpan? timeout, string objectType, params object[] objectId)
        {
            return LockManagerFactory.GetManager(objectType).AcquireLock(owner, mode, timeout, objectType, objectId);
        }

        /// <summary>
        /// Checks whether a user may read the object specified in the request. 
        /// </summary>
        /// <param name="reader">Actor trying to read the object.</param>
        /// <param name="objectType">Type of the object to check. May be null if the current instance is only used to lock a single object type.</param>
        /// <param name="objectId">ID of the object to check.</param>
        /// <returns><b>false</b> if another user has acquired an exclusive lock for the object; otherwise <b>true</b>.</returns>
        public static bool CanRead(IActor reader, string objectType, params object[] objectId)
        {
            return LockManagerFactory.GetManager(objectType).CanRead(reader, objectType, objectId);
        }

        /// <summary>
        /// Checks whether a user may modify the object specified in the request. 
        /// </summary>
        /// <param name="owner">Actor trying to modify the object.</param>
        /// <param name="objectType">Type of the object to check. May be null if the current instance is only used to lock a single object type.</param>
        /// <param name="objectId">ID of the object to check.</param>
        /// <returns><b>true</b> if the user owns a lock for the specified object; <b>false</b> otherwise.</returns>
        public static bool CanWrite(IActor owner, string objectType, params object[] objectId)
        {
            return LockManagerFactory.GetManager(objectType).CanWrite(owner, objectType, objectId);
        }

        /// <summary>
        /// Forcefully cancels a lock placed on the specified object by any user.
        /// </summary>
        /// <param name="objectType">Type of the object to unlock. May be null if the current instance is only used to lock a single object type.</param>
        /// <param name="objectId">ID of the object to unlock.</param>
        /// <remarks>This method should only be available to system administrators.</remarks>
        public static void KillLock(string objectType, params object[] objectId)
        {
            LockManagerFactory.GetManager(objectType).KillLock(objectType, objectId);
        }

        /// <summary>
        /// Releases a lock that has previously been acquired by a user.
        /// </summary>
        /// <param name="owner">Lock owner.</param>
        /// <param name="objectType">Type of the object to unlock. May be null if the current instance is only used to lock a single object type.</param>
        /// <param name="objectId">ID of the object to unlock.</param>
        public static void ReleaseLock(IActor owner, string objectType, params object[] objectId)
        {
            LockManagerFactory.GetManager(objectType).ReleaseLock(owner, objectType, objectId);
        }

        /// <summary>The key of business rule and associated message in the text source which indicates that other process or user has exclusively locked the object in the database. Value: <b>_SecurityUtil_EntityIsExclusivelyLocked</b></summary>
        public const string EntityIsExclusivelyLockedRuleKey = "_LockUtil_EntityIsExclusivelyLocked";

        /// <summary>Checks if actor can read/fetch the provided entity. Returns one broken rule if other user/process has exclusively locked the object.</summary>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="entityWithPK">Entity which the user is trying to access. Only primary key properties must be set.</param>
        /// <returns>A collection with one broken rule if another user has acquired an exclusive lock for the object; otherwise empty collection.</returns>
        public static BusinessRuleCollection CheckReadLock(IActor user, IBusinessObject entityWithPK)
        {
            BusinessRuleCollection rules = new BusinessRuleCollection();
            bool isNew = entityWithPK.IsNew && ArrayUtil.IsNullOrEmpty(entityWithPK.GetPrimaryKeyValue());
            if (isNew)
                return rules;

            bool isUsingPessimisticOfflineLocking = entityWithPK.GetDescriptor().PessimisticLockMode.HasValue;
            if (isUsingPessimisticOfflineLocking)
            {
                bool isLockedExclusively = !LockUtil.CanRead(user, entityWithPK.Table.TableName, entityWithPK.GetPrimaryKeyValue());
                if (isLockedExclusively)
                {
                    string description = entityWithPK.MessageSource.GetString(EntityIsExclusivelyLockedRuleKey) ?? Messages.LockUtil_EntityIsExclusivelyLocked;
                    rules.Add(new BusinessRule(EntityIsExclusivelyLockedRuleKey, false, description));
                }
            }

            return rules.GetBrokenRules();
        }

        /// <summary>The key of business rule and associated message in the text source which indicates that other process or user has locked the object in the database. Value: <b>_SecurityUtil_EntityIsLocked</b></summary>
        public const string EntityIsLockedRuleKey = "_LockUtil_EntityIsLocked";

        /// <summary>Checks if actor can modify the provided entity. Returns one broken rule if other user/process has locked the object.</summary>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="entity">Entity which the user is trying to modify.</param>
        /// <returns>A collection with one broken rule if another user has acquired an exclusive lock for the object; otherwise empty collection.</returns>
        public static BusinessRuleCollection CheckWriteLock(IActor user, IBusinessObject entity)
        {
            BusinessRuleCollection rules = new BusinessRuleCollection();
            bool isNew = entity.IsNew && ArrayUtil.IsNullOrEmpty(entity.GetPrimaryKeyValue());
            if (isNew)
                return rules;

            bool isUsingPessimisticOfflineLocking = entity.GetDescriptor().PessimisticLockMode.HasValue;
            if (isUsingPessimisticOfflineLocking)
            {
                bool isLocked = !LockUtil.CanWrite(user, entity.Table.TableName, entity.GetPrimaryKeyValue());
                if (isLocked)
                {
                    string description = entity.MessageSource.GetString(EntityIsExclusivelyLockedRuleKey) ?? Messages.LockUtil_EntityIsExclusivelyLocked;
                    rules.Add(new BusinessRule(EntityIsExclusivelyLockedRuleKey, false, description));
                }
            }

            return rules.GetBrokenRules();
        }

        #endregion        

        #region Optimistic offline concurrency control scheme: SetChangeTrackingPropertiesValues(), IsCurrent()

        /// <summary>Stores change tracking properties' values in <see cref="IDataObject.ChangeTrackingPropertyValues"/> for usage by 
        /// optimistic offline concurrency control scheme based on metadata specified in object's descriptor.
        /// Does nothing if no change tracking properties are defined in object's descriptor.</summary>
        /// <param name="bizObject">Business object whose change tracking properties are preserved.</param>
        public static void SetChangeTrackingPropertiesValues(IDataObject bizObject)
        {            
            string[] concurrencyTrackingPropertyNames = bizObject.GetDataObjectDescriptor().Fields.ChangeTrackingPropertyNames;
            if (ArrayUtil.IsNullOrEmpty(concurrencyTrackingPropertyNames))
                return;

            bizObject.ChangeTrackingPropertyValues.Clear();
            foreach (string trackingPropertyName in concurrencyTrackingPropertyNames)
            {
                IDbColumn trackingField = bizObject.Table.Columns.GetByPropertyName(trackingPropertyName);
                bizObject.ChangeTrackingPropertyValues[trackingField.PropertyName] = bizObject.GetField(trackingField);
            }
        }

        /// <summary>Checks whether the values of change tracking fields in data source are same as values in object's <see cref="IDataObject.ChangeTrackingPropertyValues"/> property.</summary>
        /// <param name="bizObject">Business object instance which is compared to version in the data-source.</param>
        /// <returns><b>true</b> if no other process or user has changed the object in the database or if object's <see cref="IDataObject.ChangeTrackingPropertyValues"/> dictionary is emtpty; <b>false</b> otherwise.</returns>
        /// <remarks>Typically, business transactions should be aborted if the provided object's is not based on the most recently saved data.</remarks>
        public static bool IsCurrent(IDataObject bizObject)
        {
            // Tracking values are typically empty if the concurrency control is not used or the entity is new.
            if (ArrayUtil.IsNullOrEmpty(bizObject.ChangeTrackingPropertyValues))
                return true;

            var currentTrackingProperties = FetchCurrentTrackingPropertyValues(bizObject);
            return CompareChangeTrackingProperties(bizObject, currentTrackingProperties);
        }

        /// <summary>Checks whether the values of change tracking fields in database are same as values in object's <see cref="IDataObject.ChangeTrackingPropertyValues"/> property.</summary>
        /// <param name="bizObject">Business object instance which is compared to version in the data-source.</param>
        /// <param name="checkAllChildEntites">Indicates whether all first level child entities should be checked, too. Considerably slower than checking only the root record but it might be mandatory in some data models.</param>
        /// <returns><b>true</b> if no other process or user has changed the object in the database or if object's <see cref="IDataObject.ChangeTrackingPropertyValues"/> dictionary is emtpty; <b>false</b> otherwise.</returns>
        /// <remarks>Typically, business transactions should be aborted if the provided object's is not based on the most recently saved data.</remarks>
        public static bool IsCurrent(IBusinessObject bizObject, bool checkAllChildEntites)
        {
            if (checkAllChildEntites)
                return IsEntireObjectTreeCurrent(bizObject);
            else
                return IsCurrent(bizObject);
        }

        /// <summary>The key of business rule and associated messge in the text source which indicates that the loaded object data is no longer current, i.e. other process or user has changed the data in the database. Value: <b>_SecurityUtil_EntityIsNotCurrent</b></summary>
        public const string EntityIsNotCurrentRuleKey = "_LockUtil_EntityIsNotCurrent";

        /// <summary>Checks whether the provided object instance is based on current/latest version of data. Returns one broken rule if objects's <see cref="IBusinessObject.IsCurrentVersion"/> return <b>false</b>, i.e. if other user/process has changed the object in the database.</summary>
        /// <param name="bizObject">Business object instance which is compared to version in the data-source.</param>
        /// <returns>A collection with one broken rule if object data in database has been modified since the object was fetched; otherwise empty collection.</returns>
        /// <remarks>Typically, business transactions should be aborted if the provided object's is not based on the most recently saved data.</remarks>
        public static BusinessRuleCollection CheckConcurrency(IBusinessObject bizObject)
        {
            BusinessRuleCollection brokenRules = new BusinessRuleCollection();
            bool failedOptimisticConcurrencyControl = !bizObject.IsCurrentVersion();
            if (failedOptimisticConcurrencyControl)
            {
                string description = bizObject.MessageSource.GetString(EntityIsNotCurrentRuleKey) ?? Messages.LockUtil_EntityIsNotCurrent;
                brokenRules.Add(new BusinessRule(EntityIsNotCurrentRuleKey, false, description));
            }

            return brokenRules;
        }

        /// <summary>Generates <see cref="BrokenRuleException"/> if the provided object instance is NOT based on current/latest version of data, i.e. if other user/process has changed the object in the database.</summary>
        /// <param name="bizObject">Business object instance which is compared to version in the data-source.</param>
        public static void EnforceConcurrencyControl(IBusinessObject bizObject)
        {
            var concurrencyErrors = LockUtil.CheckConcurrency(bizObject);
            if (concurrencyErrors.HasBrokenRules)
                throw new BrokenRuleException(concurrencyErrors);
        }

        private static bool IsEntireObjectTreeCurrent(IBusinessObject bizObject)
        {
            bool isRootCurrent = IsCurrent(bizObject);
            if (!isRootCurrent)
                return false;

            var descriptor = bizObject.GetDescriptor();
            foreach (var childAdapter in descriptor.Children)
            {
                foreach (IEntityCollection childCollection in childAdapter.GetChildren(bizObject))
                {
                    foreach (IDataObject child in childCollection)
                    {
                        bool isChildCurrent = IsCurrent(child);
                        if (!isChildCurrent)
                            return false;
                    }
                }
            }

            foreach (var complexPropertyAdapter in descriptor.ComplexProperties)
            {
                IDataObject childEntity = complexPropertyAdapter.GetChild(bizObject);
                if (childEntity != null)
                {
                    bool isChildCurrent = IsCurrent(childEntity);
                    if (!isChildCurrent)
                        return false;
                }
            }

            return true;
        }

        private static bool CompareChangeTrackingProperties(IDataObject bizObject, IDictionary<string, object> currentTrackingProperties)
        {
            if (currentTrackingProperties.Count != bizObject.ChangeTrackingPropertyValues.Count)
                throw new InvalidOperationException("Tracking properties count is not equal in fetched version and bizObject.ChangeTrackingPropertyValues dictionary.");

            foreach (KeyValuePair<string, object> bizobjVersion in bizObject.ChangeTrackingPropertyValues)
            {
                string propertyName = bizobjVersion.Key;
                object currentFieldValue = currentTrackingProperties[propertyName];
                bool areEqual = Convert.ToString(bizobjVersion.Value, CultureInfo.InvariantCulture) == Convert.ToString(currentFieldValue, CultureInfo.InvariantCulture);
                if (!areEqual)
                    return false;
            }

            return true;
        }

        /// <summary>Fetches current tracking properties' value from the data source.</summary>
        /// <param name="bizObject">Business object whose saved data is fetched.</param>
        /// <returns>Dictionary that contains values of properties defined in <see cref="FieldMap.ChangeTrackingPropertyNames"/> 
        /// array of object's descriptor. Empty dictionary if no change properties have been defined.</returns>
        private static IDictionary<string, object> FetchCurrentTrackingPropertyValues(IDataObject bizObject)
        {
            string[] changeTrackingPropertyNames = bizObject.GetDataObjectDescriptor().Fields.ChangeTrackingPropertyNames;
            if (ArrayUtil.IsNullOrEmpty(changeTrackingPropertyNames))
                return new Dictionary<string, object>();

            IEntity entityWithPk = bizObject.Table.NewEntity();
            foreach (IDbColumn pkField in bizObject.Table.PrimaryKey)
                entityWithPk.SetField(pkField, bizObject.GetField(pkField));

            IEntity savedVersion = OrmUtil.FetchEntity(entityWithPk);

            var changeTrackingPropertyValues =  GetChangeTrackingPropertyValue(savedVersion, changeTrackingPropertyNames);
            return changeTrackingPropertyValues;
        }

        private static IDictionary<string, object> GetChangeTrackingPropertyValue(IEntity entity, string[] changeTrackingPropertyNames)
        {
            Dictionary<string, object> changeTrackingPropertyValues = new Dictionary<string, object>();
            foreach (string trackingPropertyName in changeTrackingPropertyNames)
            {
                IDbColumn trackingField = entity.Table.Columns.GetByPropertyName(trackingPropertyName);
                changeTrackingPropertyValues[trackingField.PropertyName] = entity.GetField(trackingField);
            }

            return changeTrackingPropertyValues;
        }

        #endregion
    }
}
